// Copyright (c) 2013 Jonathan Penn (http://cocoamanifest.net/)

// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:

// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.

// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

"use strict";
#import "Env.js"

function UIAutoMonkey() {
		
	this.config = {
		//run either by minutesToRun or numberOfEvents. Only one of these can set. (To use minutes you can use config.numberOfEvents = 0)
		//minutesToRun = 60 * 8; //sample to run for 8 hours.
		//checkTimeEvery = 60; //how often to check (in events) if minutesToRun has is used. 
		numberOfEvents: 100000,
		delayBetweenEvents: 0.05,    // In seconds
		
		/**
		* Sometimes the monkey can fall into UI Holes from which it it is hard to escape. The monkey may then spend an inordinate
		* amount of time in such holes, neglecting other parts of the application.
		*
		* For example, if a parent Window P has a large image
		* and clicking on the image opens a child window C from which one exits by tapping a small X on the top right, then until that small X is
		* tapped we will reamin in C. conditionHandlers offer the developer the option to periodically recognize that we are in C and press the X.
		*
		* See buttonHandler.js for a specialized conditionHandler useful when a top level button can be used to escape from a UI hole.
		*
		* conditionHandlers are objects that respond to the following methods:
		*  isTrue(target, eventNumber): returns True if the condition is true given target and event number eventNumber.
		*  checkEvery(): How many events should pass before we check.
		*  handle(target, mainWindow) handle the condition.
		*  isExclusive() if true then if this condition's handler is invoked then processing subsequent conditions is skipped for this particular event. This
		*	 is usually set to true as it allows the condition to exit a UI hole and at that point there may be no point executing other conditions
		*  logStats() log statics using UIALogger;
		* condition handers must have the following property
		*  statsHandleInvokedCount - the count of the number of times we were invoked
		*/

		conditionHandlers: [],
		
		/**
		* Unfortunately if the application is not responsive "ANR", the monkey may not notice and continue to fire events not realizing that
		* the application is stuck. When run via continuous integration users may not notice that "successful" monkey runs in fact were in an 
		* ANR state.
		*
		* To deal with this the monkey supports ANR detection. Using an anrFingerprint function it periodically takes a fingerprint and if these
		* are identical for a specificed interval then an ANR exception is thrown. 
		*
		*
		*/
		anrSettings: {
			//fingerprintFunction defaults to false which will disable ANR fingerprinting. Otherwise set to a function that will return
			//a string. One useful idiom using tuneup.js is
			//#import tuneup.js
			//config.anrSettings.fingerprintFunction = function() {return logVisibleElementTreeJSON(false)};
	        fingerprintFunction: false,
			eventsBeforeANRDeclared: 1500, //throw exception if the fingerprint hasn't changed within this number of events
			eventsBetweenSnapshots: 150, //how often (in events) to take a snapshot using the fingerprintFunction 
			debug: false //if true extra logging is made			
		},

		// If the following line is uncommented, then screenshots are taken
		// every "n" seconds.
		screenshotInterval: 5,

		// Events are triggered based on the relative weights here. The event
		// with this highest number gets triggered the most.
		//
		// If you want to add your own "events", check out the event method
		// definitions below.
		eventWeights: {
			tap: 500,
			drag: 10,
			flick: 1,
			orientation: 1,
			clickVolumeUp: 1,
			clickVolumeDown: 1,
			lock: 1,
			pinchClose: 10,
			pinchOpen: 10,
			shake: 1
		},

		// Probability that touch events will have these different properties
		touchProbability: {
			multipleTaps: 0.05,
			multipleTouches: 0.05,
			longPress: 0.05
		},

		targetBundleId: UIATarget.localTarget().frontMostApp().bundleID(),
		orientationNum: UIATarget.localTarget().frontMostApp().interfaceOrientation().toString(),
		

		// Uncomment the following to restrict events to a rectangluar area of
		// the screen
		/*
		frame: {
			origin: {x: 0, y: 0},
			size: {width: 100, height: 50}
		}
		*/

	};

	// Dismiss alerts 
	UIATarget.onAlert = function onAlert(alert) {
		var title = alert.name();
		UIALogger.logMessage('On Alert: ' + title);
		return false;
	}

};

//Array class addon func.
Array.prototype.S=String.fromCharCode(2);
Array.prototype.in_array=function(e){
    var r=new RegExp(this.S+e+this.S);
    return (r.test(this.S+this.join(this.S)+this.S));
};

// --- --- --- ---
// Event Methods
//
// Any event probability in the hash above corresponds to a related event
// method below. So, a "tap" probability will trigger a "tap" method.
//
// If you want to add your own events, just add a probability to the hash
// above and then add a corresponding method below. Boom!
//
// Each event method can call any other method on this UIAutoMonkey object.
// All the methods at the end are helpers at your disposal and feel free to
// add your own.

UIAutoMonkey.prototype.allEvents = {
	tap: function() {
		this.target().tapWithOptions(
			{ x: this.randomX(), y: this.randomY() },
			{
				tapCount: this.randomTapCount(),
				touchCount: this.randomTouchCount(),
				duration: this.randomTapDuration()
			}
		);
	},

	drag: function() {
		this.target().dragFromToForDuration(
			{ x: this.randomX(), y: this.randomY() },
			{ x: this.randomX(), y: this.randomY() },
			0.5
		);
	},

	flick: function() {
		this.target().flickFromTo(
			{ x: this.randomX(), y: this.randomY() },
			{ x: this.randomX(), y: this.randomY() }
		);
	},

	orientation: function() {
		var orientations = [
			UIA_DEVICE_ORIENTATION_PORTRAIT,
			UIA_DEVICE_ORIENTATION_PORTRAIT_UPSIDEDOWN,
			UIA_DEVICE_ORIENTATION_LANDSCAPELEFT,
			UIA_DEVICE_ORIENTATION_LANDSCAPERIGHT
		];

		var i = Math.floor(Math.random() * 10) % orientations.length;
		var newOrientation = orientations[i];
		this.target().setDeviceOrientation(newOrientation);
		this.delay(1);
	},

	clickVolumeUp: function() {
		this.target().clickVolumeUp();
	},

	clickVolumeDown: function() {
		this.target().clickVolumeDown();
	},

	lock: function() {
		this.target().lockForDuration(Math.random() * 3);
	},

	pinchClose: function () {
		this.target().pinchCloseFromToForDuration(
			{ x: this.randomX(), y: this.randomY() },
			{ x: this.randomX(), y: this.randomY() },
			0.5
		);
	},

	pinchOpen: function () {
		this.target().pinchOpenFromToForDuration(
			{ x: this.randomX(), y: this.randomY() },
			{ x: this.randomX(), y: this.randomY() },
			0.5
		);
	},

	shake: function() {
		this.target().shake();
	}
};

// --- --- --- ---
// Helper methods
//
UIAutoMonkey.prototype.RELEASE_THE_MONKEY = function() {
	// Called at the bottom of this script to, you know...
	//
	// RELEASE THE MONKEY!
	try{
		if (this.config.minutesToRun && this.config.numberOfEvents) {
			throw "invalid configuration. You cannot define both minutesToRun and numberOfEvents"
		}
		this.saveDeviceOrientation(); //save current app orientation number.
		var conditionHandlers = this.config.conditionHandlers || []; //For legacy configs, if not present default to empty.
		var useConditionHandlers = conditionHandlers.length > 0;
		var checkTime = false;
		var localNumberOfEvents = this.config.numberOfEvents; //we may modify so we want to leave config untouched
		if (this.config.minutesToRun) {
			checkTime = true;
			localNumberOfEvents = 2000000000;
			var startTime = new Date().getTime();
			var checkTimeEvery = this.config.checkTimeEvery || 60; //number of events to pass before we check the time
		}
		//setup anr parameters as needed
		var anrFingerprintFunction = this.config.anrSettings ? this.config.anrSettings.fingerprintFunction : false; //handle legacy settings missing this
		if (anrFingerprintFunction) {
			this.anrSnapshot = "Initial snapshot-nothing should match this!!";
			this.anrSnapshotTakenAtIndex = -1;		
			var anrEventsBetweenSnapshots = this.config.anrSettings.eventsBetweenSnapshots || 300;
			var anrDebug = this.config.anrSettings.debug;
			this.anrMaxElapsedCount = -1;
	    } 

	    UIALogger.logMessage(JSON.stringify(UIATarget.localTarget().rect().size));
	    	    
		for (var i = 0; i < localNumberOfEvents; i++) {
			if (checkTime && (i % checkTimeEvery == 0)) { //check the time if needed
				var currTime = new Date().getTime();
				var elapsedMinutes = (currTime-startTime) / 60000;
				if (elapsedMinutes >= this.config.minutesToRun) {
					UIALogger.logDebug("Ending monkey after " + elapsedMinutes + " minutes run time.");
					break;
				} else {
					UIALogger.logDebug(this.config.minutesToRun - elapsedMinutes + " minutes left to run.")
				}
			}

			if (this.target().frontMostApp().bundleID() !== this.config.targetBundleId) this.reLaunchApp();

			this.triggerRandomEvent();
			if (anrFingerprintFunction && (i % anrEventsBetweenSnapshots == 0)) this.anrCheck(i, anrFingerprintFunction, anrDebug);
			// if (this.config.screenshotInterval) this.takeScreenShotIfItIsTime();
			// this.takeScreenShotIfItIsTime();
			if (useConditionHandlers) this.processConditionHandlers(conditionHandlers, i+1, this.target());
			this.delay();
		}
		// publish stats if warranted
		if (anrFingerprintFunction) {
			UIALogger.logDebug("ANR Statistics");
			UIALogger.logDebug("ANR max event count for identical fingerprint snapshots :: events before ANR declared: " + this.anrMaxElapsedCount + " :: " + this.config.anrSettings.eventsBeforeANRDeclared);
		}

		if (useConditionHandlers) {
			UIALogger.logDebug("ConditionHandler Statistics")
			conditionHandlers.forEach(function(aHandler) {aHandler.logStats()});
			conditionHandlers.sort(function(aHandler, bHandler) {return aHandler.statsHandleInvokedCount - bHandler.statsHandleInvokedCount});
			UIALogger.logDebug("sorted by HandleInvokedCount");
			conditionHandlers.forEach(function(aHandler) {UIALogger.logDebug(aHandler + ": " + aHandler.statsHandleInvokedCount)});
		}
	}finally{
		UIALogger.logDebug("MonkeyTest finish.");
	}
};


UIAutoMonkey.prototype.anrCheck = function(i, fingerprintFunction, debugFlag){
	var newSnapshot = fingerprintFunction();
	if (newSnapshot != this.anrSnapshot) {
		//all is good, we're moving along
		if (debugFlag) UIALogger.logDebug("UIAutoMonkey:anrCheck(): snapshot != for event " + i);
		this.anrSnapshot = newSnapshot;
		this.anrSnapshotTakenAtIndex = i;
	} 
	else {
		//have a match
		//for how many counts?
		var elapsedCount = i - this.anrSnapshotTakenAtIndex;
		this.anrMaxElapsedCount = Math.max(this.anrMaxElapsedCount, elapsedCount);
		UIALogger.logDebug("UIAutoMonkey:anrCheck(): snapshot == with elapsed count=" + elapsedCount);
		if (elapsedCount > this.config.anrSettings.eventsBeforeANRDeclared) {
			if (this.config.anrSettings.debug){
				UIALogger.logDebug("duplicate snapshot detected" + this.anrSnapshot);
			}
			throw "anr exception-identical after " + elapsedCount + " events";
		};
	};
};


UIAutoMonkey.prototype.processConditionHandlers = function(conditionHandlers, eventNumberPlus1, target) {
	var mainWindow = target.frontMostApp().mainWindow(); //optimization to let handlers do less work. Assumes isTrue() doesn't alter the mainWindow.
	for (var i = 0; i < conditionHandlers.length; i++) {
		var aCondition = conditionHandlers[i];
		if ((eventNumberPlus1 % aCondition.checkEvery()) != 0) {
			continue; //not yet time to process aCondition.
		}
		try {
			UIATarget.localTarget().pushTimeout(0);
			var isConditionTrue = aCondition.isTrue(target, eventNumberPlus1, mainWindow);
		}
		finally {
		    UIATarget.localTarget().popTimeout();
		}
		if (isConditionTrue) {
			aCondition.handle(target, mainWindow);
			this.takeScreenShotIfItIsTime();
			if (aCondition.isExclusive()) {
				break;
			} else {
				mainWindow = target.frontMostApp().mainWindow(); //could be stale
			}
		};
	};
};

UIAutoMonkey.prototype.triggerRandomEvent = function() {
	var name = this.chooseEventName();
	// Find the event method based on the name of the event
	var event = this.allEvents[name];
	event.apply(this);
	this.takeScreenShotIfItIsTime();
};

UIAutoMonkey.prototype.target = function() {
	// Return the local target.
	return UIATarget.localTarget();
};

UIAutoMonkey.prototype.delay = function(seconds) {
	// Delay the target by `seconds` (can be a fraction)
	// Defaults to setting in configuration
	seconds = seconds || this.config.delayBetweenEvents;
	this.target().delay(seconds);
};

UIAutoMonkey.prototype.chooseEventName = function() {
	// Randomly chooses an event name from the `eventsWeight` dictionary
	// based on the given weights.
	var calculatedEventWeights = [];
	var totalWeight = 0;
	var events = this.config.eventWeights;
	for (var event in events) {
		if (events.hasOwnProperty(event)) {
			calculatedEventWeights.push({
				weight: events[event]+totalWeight,
				event: event
			});
			totalWeight += events[event];
		}
	}

	var chosenWeight = Math.random() * 1000 % totalWeight;

	for (var i = 0; i < calculatedEventWeights.length; i++) {
		if (chosenWeight < calculatedEventWeights[i].weight) {
			return calculatedEventWeights[i].event;
		}
	}

	throw "No event was chosen!";
};

UIAutoMonkey.prototype.screenWidth = function() {
	// Need to adjust by one to stay within rectangle
	return parseInt(this.target().rect().size.width - 1);
};

UIAutoMonkey.prototype.screenHeight = function() {
	// Need to adjust by one to stay within rectangle
	return parseInt(this.target().rect().size.height - 1);
};

UIAutoMonkey.prototype.randomX = function() {
	var min, max;	
	var r = Math.random();

	if (this.config.frame){
		if(this.screenRectSafe()){
			// Limits coordinates to given frame if set in config
			min = this.config.frame.origin.x;
			max = this.config.frame.size.width + min;
		}else{
			// Limits coordinates to given frame if set in config
			min = parseInt(this.target().rect().origin.x);
			max = this.screenWidth() + min;
		}
	} else {
		// Returns a random X coordinate within the screen rectangle
		min = 0;
		max = this.screenWidth();
	}
	return r == 0 ? 1 : r * (max - min) + min;
};

UIAutoMonkey.prototype.randomY = function() {
	var min, max;
	var r = Math.random();

	if (this.config.frame){
		if(this.screenRectSafe()){
			// Limits coordinates to given frame if set in config
			min = this.config.frame.origin.y;
			max = this.config.frame.size.height + min;
		}else{
			// Limits coordinates to given frame if set in config
			min = parseInt(this.target().rect().origin.y);
			max = this.screenHeight() + min;
		}
	} else {
		// Returns a random Y coordinate within the screen rectangle
		min = 0;
		max = this.screenHeight();
	}
	return r == 0 ? 1 : r * (max - min) + min;
};

UIAutoMonkey.prototype.randomTapCount = function() {
	// Calculates a tap count for tap events based on touch probabilities
	if (this.config.touchProbability.multipleTaps > Math.random()) {
		return Math.floor(Math.random() * 10) % 3 + 1;
	}
	else return 1;
};

UIAutoMonkey.prototype.randomTouchCount = function() {
	// Calculates a touch count for tap events based on touch probabilities
	if (this.config.touchProbability.multipleTouches > Math.random()) {
		return Math.floor(Math.random() * 10) % 3 + 1;
	}
	else return 1;
};

UIAutoMonkey.prototype.randomTapDuration = function() {
	// Calculates whether or not a tap should be a long press based on
	// touch probabilities
	if (this.config.touchProbability.longPress > Math.random()) {
		return 0.5;
	}
	else return 0;
};

UIAutoMonkey.prototype.randomRadians = function() {
	// Returns a random radian value
	return Math.random() * 10 % (3.14159 * 2);
};

UIAutoMonkey.prototype.takeScreenShotIfItIsTime = function() {
	// var now = (new Date()).valueOf();
	// if (!this._lastScreenshotTime) this._lastScreenshotTime = 0;

	// if (now - this._lastScreenshotTime > this.config.screenshotInterval * 1000) {
	// 	var filename = "monkey-" + (new Date()).toISOString().replace(/[:\.]+/g, "-");
	// 	this.target().captureScreenWithName(filename);
	// 	this._lastScreenshotTime = now;
	// }
	var filename = "monkey-" + (new Date()).toISOString().replace(/[:\.]+/g, "-");
	this.target().captureScreenWithName(filename);
	if (this.target().frontMostApp().bundleID() !== this.config.targetBundleId) this.reLaunchApp();
};

UIAutoMonkey.prototype.reLaunchApp = function() {
	UIALogger.logWarning("Target app go to outside, trigger re-launch action.");
	var result = this.target().host().performTaskWithPathArgumentsTimeout("/usr/bin/which", ["idevicedebug"], 5);
	var idevicedebug_original_path = result.stdout.trim();
	this.target().host().performTaskWithPathArgumentsTimeout(idevicedebug_original_path, ["-u", UniqueDeviceID, "run", this.config.targetBundleId, ">/dev/null", "2>&1", "&"], 5);
};

UIAutoMonkey.prototype.saveDeviceOrientation = function() {
	var saveFile = ResultBaseDir + "/" + "orientation";
	var command = "echo " + this.config.orientationNum + " > " + saveFile
	this.target().host().performTaskWithPathArgumentsTimeout("/bin/bash", ["-c", command], 5);
};

UIAutoMonkey.prototype.screenRectSafe = function() {
	var vertical_arr = ["1", "2"];
	var horizontal_arr = ["3" ,"4"];
	var current_orientation = this.target().frontMostApp().interfaceOrientation().toString();
	var config_orientation = this.config.orientationNum;
	var condition_allin_vertical = vertical_arr.in_array(current_orientation) && vertical_arr.in_array(config_orientation);
	var condition_allin_horizontal = horizontal_arr.in_array(current_orientation) && horizontal_arr.in_array(config_orientation);
	return condition_allin_vertical || condition_allin_horizontal
};

// Commodity function to call RELEASE_THE_MONKEY directly on UIAutoMonkey
// if you don't need to customize your instance
UIAutoMonkey.RELEASE_THE_MONKEY = function() {
	(new UIAutoMonkey()).RELEASE_THE_MONKEY();
};

// UIAutoMonkey.RELEASE_THE_MONKEY();
